package eu.darken.octi.sync.module.syncs.jserver.core

import com.gitee.wsl.exception.asLog
import com.gitee.wsl.exception.logTag
import com.gitee.wsl.android.ext.ApplicationInit.application
import com.gitee.wsl.android.ext.device.networkStateFlow
import com.gitee.wsl.flow.DynamicStateFlow
import com.gitee.wsl.flow.setupCommonEventHandlers
import com.gitee.wsl.jvm.okio.fromGzip
import com.gitee.wsl.jvm.okio.toGzip

import eu.darken.octi.sync.module.core.ModuleId
import eu.darken.octi.sync.core.*
import eu.darken.octi.sync.core.encryption.PayloadEncryption
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okio.ByteString
import retrofit2.HttpException
import timber.log.Timber
import java.time.Duration
import java.time.Instant
import kotlin.math.max


@Suppress("BlockingMethodInNonBlockingContext")
class JServerConnector  constructor(
    val credentials: JServer.Credentials,
    private val scope: CoroutineScope,
    private val dispatcherProvider: CoroutineDispatcher=Dispatchers.IO,
    private val endpointFactory: JServerEndpoint.Factory,
    private val networkStateProvider: Flow<Boolean> = application.networkStateFlow(),
    private val syncSettings: SyncSettings,
    private val supportedModuleIds: Set<@JvmSuppressWildcards ModuleId>,
) : SyncConnector {

    private val endpoint by lazy {
        endpointFactory.create(credentials.serverAdress).also {
            it.setCredentials(credentials)
        }
    }

    private val crypti by lazy { PayloadEncryption(credentials.encryptionKeyset) }

    data class State(
        override val readActions: Int = 0,
        override val writeActions: Int = 0,
        override val lastReadAt: Instant? = null,
        override val lastWriteAt: Instant? = null,
        override val lastError: Exception? = null,
        override val quota: SyncConnectorState.Quota? = null,
        override val devices: Collection<DeviceId>? = null,
        override val isAvailable: Boolean = true,
    ) : SyncConnectorState

    private val _state = DynamicStateFlow(
        parentScope = scope + dispatcherProvider,
        loggingTag = TAG,
    ) {
        State()
    }

    override val state: Flow<State> = _state.flow
    private val _data = MutableStateFlow<SyncRead?>(null)
    override val data: Flow<SyncRead?> = _data

    private val writeQueue = MutableSharedFlow<SyncWrite>()
    private val writeLock = Mutex()
    private val readLock = Mutex()

    override val identifier: ConnectorId = ConnectorId(
        type = "jserver",
        subtype = credentials.serverAdress.domain,
        account = credentials.accountId.id,
    )

    init {
        writeQueue
            .onEach { toWrite ->
                writeServerWrapper {
                    writeServer(toWrite)
                }
            }
            .retry {
                delay(5000)
                true
            }
            .setupCommonEventHandlers(TAG) { "writeQueue" }
            .launchIn(scope)
    }

    private suspend fun isInternetAvailable() = networkStateProvider.first()

    override suspend fun write(toWrite: SyncWrite) {
        Timber.d( "write(toWrite=$toWrite)" )
        writeQueue.emit(toWrite)
    }

    override suspend fun deleteAll() = writeServerWrapper {
        Timber.d( "deleteAll()" )
        val deviceIds = endpoint.listDevices()
        Timber.d( "deleteAll(): Found devices: $deviceIds" )

        deviceIds.forEach {
            Timber.d( "deleteAll(): Deleting module data for $it" )
            try {
                endpoint.deleteModules(it)
            } catch (e: Exception) {
                Timber.w( "Failed to delete $it" )
            }
        }
    }

    override suspend fun deleteDevice(deviceId: DeviceId) = writeServerWrapper {
        Timber.d( "deleteDevice(deviceId=$deviceId)" )
        try {
            endpoint.deleteModules(deviceId)
        } catch (e: HttpException) {
            // TODO once we have device deletion as an API, remove this edge case catch
            if (e.code() == 401) {
                Timber.w( "Can't delete device because we are not authorized")
            } else {
                throw e
            }
        }
    }

    suspend fun createLinkCode(): LinkingData {
        Timber.d( "createLinkCode()" )
        val linkCode = endpoint.createLinkCode()

        return LinkingData(
            serverAdress = credentials.serverAdress,
            linkCode = linkCode,
            encryptionKeyset = credentials.encryptionKeyset,
        )
    }

    override suspend fun sync(options: SyncOptions) {
        Timber.d( "sync(options=$options)" )

        if (!isInternetAvailable()) {
            Timber.w( "sync(): Skipping, we are offline." )
            return
        }

        if (options.writeData) {
            // TODO
        }

        if (options.readData) {
            Timber.d( "read()" )
            try {
                readServerWrapper {
                    _data.value = readServer()
                }
            } catch (e: Exception) {
                Timber.e( "Failed to read: ${e.asLog()}" )
                _state.updateBlocking { copy(lastError = e) }
            }
        }

        if (options.stats) {
            try {
                val knownDeviceIds = endpoint.listDevices()
                _state.updateBlocking { copy(devices = knownDeviceIds) }
            } catch (e: Exception) {
                Timber.e( "Failed to list of known devices: ${e.asLog()}")
            }
        }
    }

    suspend fun checkHealth(): JServerApi.Health {
        Timber.d( "checkHealth()" )
        return endpoint.getHealth()
    }

    private suspend fun fetchModule(deviceId: DeviceId, moduleId: ModuleId): JServerModuleData? {
        val readData = endpoint.readModule(deviceId = deviceId, moduleId = moduleId) ?: return null

        val payload = if (readData.payload != ByteString.EMPTY) {
            crypti.decrypt(readData.payload).fromGzip()
        } else {
            ByteString.EMPTY
        }

        return JServerModuleData(
            connectorId = identifier,
            deviceId = deviceId,
            moduleId = moduleId,
            modifiedAt = readData.modifiedAt,
            payload = payload,
        ).also { Timber.d( "readServer(): Module data: $it" ) }
    }

    private suspend fun readServer(): JServerData {
        Timber.d( "readServer(): Starting..." )
        val deviceIds = endpoint.listDevices()
        Timber.d( "readServer(): Found devices: $deviceIds" )

        val devices = deviceIds.map { deviceId ->
            scope.async moduleFetch@{
                val moduleFetchJobs = supportedModuleIds.map { moduleId ->
                    val fetchResult = try {
                        fetchModule(deviceId, moduleId)
                    } catch (e: Exception) {
                        Timber.e( "Failed to fetch: $deviceId:$moduleId:\n${e.asLog()}" )
                        null
                    }
                    Timber.d( "Module fetched: $fetchResult" )
                    fetchResult
                }

                val modules = moduleFetchJobs.filterNotNull()

                JServerDeviceData(
                    deviceId = deviceId,
                    modules = modules,
                )
            }
        }.awaitAll()

        return JServerData(
            connectorId = identifier,
            devices = devices
        )
    }

    private suspend fun writeServer(data: SyncWrite) {
        Timber.d( "writeServer(): $data)" )

        // TODO cache write data for when we are online again?
        if (!isInternetAvailable()) {
            Timber.w( "writeServer(): Skipping, we are offline.")
            return
        }

        data.modules.forEach { module ->
            endpoint.writeModule(
                moduleId = module.moduleId,
                payload = crypti.encrypt(module.payload.toGzip()),
            )
        }
        Timber.d( "writeServer(): Done" )
    }

    private fun getStorageStats(): SyncConnectorState.Quota {
        Timber.d( "getStorageStats()")

        return SyncConnectorState.Quota()
    }

    private suspend fun readServerWrapper(block: suspend () -> Unit) {
        val start = System.currentTimeMillis()
        Timber.d( "readAction(block=$block)" )

        var newStorageQuota: SyncConnectorState.Quota? = null

        if (_state.value().readActions > 0) {
            Timber.w( "Already executing read skipping.")
            return
        }
        try {
            _state.updateBlocking {
                copy(readActions = readActions + 1)
            }

            block()

            val lastStats = _state.value().quota?.updatedAt
            if (lastStats == null || Duration.between(lastStats, Instant.now()) > Duration.ofSeconds(60)) {
                Timber.d( "readAction(block=$block): Updating storage stats" )
                newStorageQuota = getStorageStats()
            }
        } catch (e: Exception) {
            Timber.e( "readAction(block=$block) failed: ${e.asLog()}")
            throw e
        } finally {
            _state.updateBlocking {
                copy(
                    readActions = max(readActions - 1, 0),
                    quota = newStorageQuota ?: quota,
                    lastReadAt = Instant.now(),
                )
            }
        }

        Timber.d( "readAction(block=$block) finished after ${System.currentTimeMillis() - start}ms" )
    }

    private suspend fun writeServerWrapper(block: suspend () -> Unit) = withContext(NonCancellable) {
        val start = System.currentTimeMillis()
        Timber.d( "writeAction(block=$block)" )

        _state.updateBlocking { copy(writeActions = writeActions + 1) }

        try {
            writeLock.withLock {
                try {
                    block()
                } catch (e: Exception) {
                    Timber.e( "writeAction(block=$block) failed: ${e.asLog()}")
                    throw e
                }
            }
        } finally {
            _state.updateBlocking {
                Timber.d( "writeAction(block=$block) finished" )
                copy(
                    writeActions = writeActions - 1,
                    lastWriteAt = Instant.now(),
                )
            }
            Timber.d( "writeAction(block=$block) finished after ${System.currentTimeMillis() - start}ms" )
        }
    }

    interface Factory {
        fun create(account: JServer.Credentials): JServerConnector
    }

    companion object{
        private val TAG = logTag("Sync", "JServer", "Connector")
    }
}